/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package sismol.data;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Persistence;
import javax.persistence.Query;
import sismol.cartography.DiaSemana;
import sismol.cartography.Linha;
import sismol.cartography.Onibus;
import sismol.cartography.Ponto;
import sismol.cartography.Trecho;
import sismol.cartography.TrechoAtual;
import sismol.cartography.TempoMedio;
import sismol.cartography.Terminal;

/**
 * @author koji
 */
public class GerenciadorEstimativa {

    private EntityManager em = Persistence.createEntityManagerFactory("sismolPU").createEntityManager();

    public Boolean processaInfo(InfoTrajetoria info) {
        Boolean retorno;
        Double latitude = info.getLatitude();
        Double longitude = info.getLongitude();

        Long onibusId = info.getOnibusId();
        Date dataAtual = info.getHorario();

        TrechoAtual trechoAtual = this.buscaTrechoAtualPorOnibusId(onibusId);
        Trecho trecho = trechoAtual.getTrecho();
        // Verifica se "chegou" (analisar o erro) ao ponto.
        if (trecho.getPontoB().proximo(latitude, longitude)) {
            // Chegou ao ponto B.

            // Calcula o tempo que demorou.
            Long chegada = dataAtual.getTime();
            Long saida = trechoAtual.getHoraUltimoPonto();
            Long demorou = chegada - saida;

            // Atualiza a TempoMedio.
            TempoMedio tempoMedio = this.buscaTempoMedioDoTrecho(trechoAtual.getTrecho().getId());
            Long media;
            if (tempoMedio.getTempoMedio() != null) {
                media = tempoMedio.getTempoMedio();
            } else {
                media = demorou;
            }
            
            Long soma = tempoMedio.getSomaTotal();
            Long novaMedia = ((media*soma) + demorou) / (soma+1);

            tempoMedio.setTempoMedio(novaMedia);
            tempoMedio.setSomaTotal(soma + 1);

            this.em.getTransaction().begin();
            this.em.merge(tempoMedio);
            this.em.getTransaction().commit();
            
            // Atualiza o TrechoAtual.
            Linha linha = trecho.getLinha();
            Trecho proximoTrecho = this.proximoTrechoDaLinha(linha, trecho);

            trechoAtual.setHoraUltimoPonto(dataAtual.getTime());
            trechoAtual.setDistanciaPercorrida(0L);
            trechoAtual.setTrecho(proximoTrecho);

            retorno = Boolean.TRUE;
        } else {
            trechoAtual.setDistanciaPercorrida(info.getDistanciaPercorrida());
            retorno = Boolean.FALSE;
        }

        trechoAtual.setLongitude(longitude);
        trechoAtual.setLatitude(latitude);
        trechoAtual.setHoraUltimoSinal(dataAtual.getTime());
        trechoAtual.setVelocidade(info.getVelocidade());

        this.em.getTransaction().begin();
        this.em.merge(trechoAtual);
        this.em.getTransaction().commit();

        return retorno;
    }

    public Trecho proximoTrechoDaLinha(Linha linha, Trecho trecho) {
        List<Trecho> trechos = linha.getTrechos();
        if (!trechos.contains(trecho)) {
            throw new RuntimeException("Trecho não pertence à linha");
        }

        int index = trechos.indexOf(trecho);
        return trechos.get((index+1) % trechos.size());
    }

    public String buscaTempoEstimadoAtePonto(Long linhaId, Long pontoId) {
        Trecho trechoInicial = null;
        Trecho trechoFinal = null;

        StringBuilder sb = new StringBuilder();
        sb.append("SELECT x FROM " + Trecho.class.getSimpleName() + " x WHERE x.pontoB.id = :pontoId AND x.linha.id = :linhaId");
        Query query = this.em.createQuery(sb.toString());
        query.setParameter("linhaId", linhaId);
        query.setParameter("pontoId", pontoId);

        try{
            trechoFinal = (Trecho) query.getSingleResult();
        } catch(Exception e) {
            throw new RuntimeException("A linha de ônibus escolhida não passa pelo ponto.");
        }

        Linha linha = this.em.find(Linha.class, linhaId);
        Ponto ponto = this.em.find(Ponto.class, pontoId);

        TrechoAtual trechoAtual = this.buscaOnibusMaisProximo(linha, pontoId);

        Long segundos;
        if (trechoAtual == null) {
            segundos = this.segundosAteProximoOnibus(linha, ponto);
        } else {
            trechoInicial = trechoAtual.getTrecho();
            List<Trecho> trechosDaLinha = linha.getTrechos();
            segundos = this.calculaTempoEstimado(trechoAtual, trechoInicial, trechoFinal, trechosDaLinha);
        }
        return this.segundosToString(segundos);
    }

    private String segundosToString(Long seg) {
        StringBuilder sb = new StringBuilder();
        if (seg > 60) {
            Long segundos = seg % 60;
            Long minutos = seg / 60;

            if (minutos != null && minutos > 0) {
                sb.append(minutos).append(" Minutos ");
            }
            if (segundos != null && segundos > 0) {
                sb.append(segundos).append(" Segundos");
            }
        } else {
            sb.append(seg).append(" Segundos");
        }
        return sb.toString();
    }
    private Long calculaTempoEstimado(TrechoAtual posicaoAtual, Trecho inicio, Trecho fim, List<Trecho> trechosDaLinha) {

        // Cálculo do tempo que o ônibus vai demorar pra chegar no próximo ponto dele. A partir do próximo ponto, são
        // considerados apenas os valores de tempo médio salvos.
        Long distanciaPercorrida = posicaoAtual.getDistanciaPercorrida();
        Long distanciaTotal = inicio.getDistancia();
        Long distanciaFalta = distanciaTotal - distanciaPercorrida;

        Double velocidade;
        Double velocidadeAtual = posicaoAtual.getVelocidade();
        TempoMedio tempoMedio = this.buscaTempoMedioDoTrecho(inicio.getId());
        Double velocidadeMedia = 0D;
        if (tempoMedio.getTempoMedio() != null) {
            velocidadeMedia = (distanciaTotal.doubleValue()*1000) / (tempoMedio.getTempoMedio());
        } else {
            // TODO: Tratar melhor esse caso
            // Default: 15 m/s
            velocidadeMedia = 10D;
        }
        if (velocidadeAtual > 0) {
            // Velocidade calculada com o peso da distancia que já foi percorrida.
            velocidade = velocidadeAtual * (distanciaPercorrida.doubleValue() / distanciaTotal.doubleValue()) + velocidadeMedia * (distanciaFalta.doubleValue() / distanciaTotal.doubleValue());
        } else {
            velocidade = velocidadeMedia;
        }

        Long tempoAteProximoPonto = Math.round(distanciaFalta / velocidade);

        // Somatória do tempo gasto nos trechos que o ônibus ainda têm que percorrer.
        Long somaOutrosPontos = 0L;

        int indexinicio = trechosDaLinha.indexOf(inicio);
        int indexfim = trechosDaLinha.indexOf(fim);
        int index = indexfim;

        while (index != indexinicio) {
            Trecho trecho = trechosDaLinha.get(index);
            TempoMedio tempoMedioRestante = this.buscaTempoMedioDoTrecho(trecho.getId());
            Long tempo = 0L;
            if (tempoMedioRestante.getTempoMedio() != null) {
                tempo = tempoMedioRestante.getTempoMedio() / 1000;
            } else {
                tempo = Math.round(trecho.getDistancia() / velocidade);
            }
            somaOutrosPontos = somaOutrosPontos + tempo;

            index = index - 1;
            if (index < 0) {
                index = trechosDaLinha.size() - 1;
            }
        }


//        List<Trecho> trechosRestantes = trechosDaLinha.subList(trechosDaLinha.indexOf(inicio)+1, trechosDaLinha.indexOf(fim)+1);
//        for (Trecho trecho : trechosRestantes) {
//            TempoMedio tempoMedioRestante = this.buscaTempoMedioDoTrecho(trecho.getId());
//            Long tempo = 0L;
//            if (tempoMedioRestante.getTempoMedio() != null) {
//                tempo = tempoMedioRestante.getTempoMedio() / 1000;
//            } else {
//                tempo = Math.round(trecho.getDistancia() / velocidade);
//            }
//            somaOutrosPontos = somaOutrosPontos + tempo;
//        }

        return tempoAteProximoPonto + somaOutrosPontos;
    }

    private TrechoAtual buscaOnibusMaisProximo(Linha linha, Long pontoId) {
        StringBuilder sb = new StringBuilder();
        sb.append("SELECT x FROM " + Trecho.class.getSimpleName() + " x WHERE x.pontoB.id = :pontoId AND x.linha = :linha");
        Query query = this.em.createQuery(sb.toString());
        query.setParameter("linha", linha);
        query.setParameter("pontoId", pontoId);

        Trecho trecho = null;
        try{
            trecho = ((List<Trecho>) query.getResultList()).get(0);
        } catch(Exception e) {
            throw new RuntimeException("O ponto não pertence à linha.");
        }

        sb = new StringBuilder();
        sb.append("SELECT x FROM " + TrechoAtual.class.getSimpleName() + " x WHERE x.onibus.linha = :linha AND x.trecho = :trecho ORDER BY x.distanciaPercorrida DESC");
        query = this.em.createQuery(sb.toString());
        query.setParameter("linha", linha);

        TrechoAtual trechoAtual = null;
        List<Trecho> trechos = linha.getTrechos();
        int index = trechos.indexOf(trecho);
        int indexInicial = index;
        do {
            Trecho proximoTrecho = trechos.get(index);
            query.setParameter("trecho" , proximoTrecho);
            try {
                trechoAtual = (TrechoAtual) query.getResultList().get(0);
            } catch(Exception e) {
                System.out.println(e.getMessage());
                trechoAtual = null;
            }
            index = (trechos.indexOf(proximoTrecho) - 1);
            if (index < 0) {
                index = trechos.size() - 1;
            }
            if (index == indexInicial) {
                break;
            }
        } while(trechoAtual == null);

//        while(trechoAtual == null) {
//            if (index < 0) {
//                break;
//            }
//            Trecho proximoTrecho = trechos.get(index);
//            query.setParameter("trecho" , proximoTrecho);
//            try {
//                trechoAtual = (TrechoAtual) query.getResultList().get(0);
//            } catch(Exception e) {
//                System.out.println(e.getMessage());
//                trechoAtual = null;
//            }
//            index--;
//        }

        return trechoAtual;
    }

    private Long segundosAteProximoOnibus(Linha linha, Ponto ponto) {
        Terminal terminal = this.terminalMaisProximo(linha, ponto);

        // Encontra próximo horário de saída de ônibus
        Collection<Long> saidas = terminal.getSaidas();
        Long tempoAteProximo = Long.MAX_VALUE;
        Date agora = new Date();
        Long agoraMs = agora.getTime();
        for (Long saida : saidas) {
            Date data = new Date(saida);
            Calendar c = new GregorianCalendar();
            c.setTime(data);
            int hour = c.get(Calendar.HOUR_OF_DAY);
            int min = c.get(Calendar.MINUTE);

            c.setTime(agora);
            c.set(Calendar.HOUR_OF_DAY, hour);
            c.set(Calendar.MINUTE, min);

            Long date = c.getTime().getTime();
            Long dif = date - agoraMs;
            if (dif > 0 && dif < tempoAteProximo) {
                tempoAteProximo = dif;
            }
        }

        // Calcula o tempo medio para o restante do percurso
        Ponto pontoTerminal = terminal.getTerminal();
        List<Trecho> trechos = linha.getTrechos();
        Long tempoPercurso = 0L;
        Boolean emPercurso = false;
        for (Trecho trecho : trechos) {
            if (trecho.getPontoA().equals(pontoTerminal)) {
                emPercurso = true;
            }
            if (emPercurso) {
                TempoMedio tempoMedio = this.buscaTempoMedioDoTrecho(trecho.getId());
                tempoPercurso += tempoMedio.getTempoMedio();
            }
            if (trecho.getPontoB().equals(ponto)) {
                break;
            }
        }

        return tempoAteProximo + tempoPercurso;
    }

    private Terminal terminalMaisProximo(Linha linha, Ponto ponto) {
        StringBuilder sb = new StringBuilder();


        List<Trecho> trechos = linha.getTrechos();
        List<Trecho> trechosAntes = new ArrayList<Trecho>();
        Trecho destino = null;
        for (Trecho trecho : trechos) {
            if (trecho.getPontoB().equals(ponto)) {
                destino = trecho;
                break;
            }
        }


        sb.append("SELECT x FROM ").append(Terminal.class.getSimpleName()).append(" x WHERE x.terminal = :ponto");
        Query query = this.em.createQuery(sb.toString());
        query.setParameter("ponto", ponto);

        List<Terminal> terminais = (List<Terminal>) query.getResultList();


        Integer index = trechos.indexOf(destino);
        for (int i = index; i >= 0; i--) {
            Trecho trecho = trechos.get(i);
            for (Terminal terminal : terminais) {
                if (terminal.getTerminal().equals(trecho.getPontoA())) {
                    return terminal;
                }
            }
        }
        return null;
    }

    private TempoMedio buscaTempoMedioDoTrecho(Long trechoId) {
        Trecho trecho = this.em.find(Trecho.class, trechoId);

        StringBuilder sb = new StringBuilder();
        sb.append("SELECT x FROM ").append(TempoMedio.class.getSimpleName());
        sb.append(" x WHERE x.trecho = :trecho");
        sb.append(" AND x.diaDaSemana = :diaDaSemana");
        // TODO: Colocar busca por Feriado e Véspera.

        Query query = this.em.createQuery(sb.toString());
        query.setParameter("trecho", trecho);
        query.setParameter("diaDaSemana", this.diaDaSemana());

        TempoMedio tempoMedio;
        try{
             tempoMedio = (TempoMedio) query.getSingleResult();
        } catch(NoResultException e){
            tempoMedio = this.criarTempoMedio(trecho);
        }

        return tempoMedio;
    }

    private TempoMedio criarTempoMedio(Trecho trecho){
        TempoMedio tempoMedio = new TempoMedio();
        tempoMedio.setTrecho(trecho);
        tempoMedio.setDiaDaSemana(this.diaDaSemana());
        tempoMedio.setSomaTotal(0L);
        tempoMedio.setTempoMedio(null);

        this.em.getTransaction().begin();
        this.em.persist(tempoMedio);
        this.em.getTransaction().commit();

        return tempoMedio;
    }

    public DiaSemana diaDaSemana() {
        Date agora = new Date();
        Calendar c = new GregorianCalendar();

        c.setTime(agora);
        int i = c.get(Calendar.DAY_OF_WEEK);
        switch(i){
            case 1:
                return DiaSemana.DOMINGO;
            case 2:
                return DiaSemana.SEGUNDA;
            case 3:
                return DiaSemana.TERCA;
            case 4:
                return DiaSemana.QUARTA;
            case 5:
                return DiaSemana.QUINTA;
            case 6:
                return DiaSemana.SEXTA;
            case 7:
                return DiaSemana.SABADO;
        }
        return null;
    }

    public TrechoAtual buscaTrechoAtualPorOnibusId(Long onibusId) {
        StringBuilder sb = new StringBuilder();
        sb.append("SELECT x FROM ").append(TrechoAtual.class.getSimpleName());
        sb.append(" x WHERE x.onibus.id = :onibusId");

        Query query = this.em.createQuery(sb.toString());
        query.setParameter("onibusId", onibusId);

        TrechoAtual retorno = (TrechoAtual) query.getSingleResult();
        this.em.refresh(retorno);
        return retorno;
    }
    
    public Collection<Onibus> listaOnibusDaLinha(Linha linha) {
        StringBuilder sb = new StringBuilder();
        sb.append("SELECT x FROM ").append(Onibus.class.getSimpleName());
        sb.append(" x WHERE x.linha = :linha");

        Query query = this.em.createQuery(sb.toString());
        query.setParameter("linha", linha);

        return query.getResultList();
    }
    
}
